From 4db1a5f5c4216cef20afe872ea99264c63b72eb3 Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Tue, 27 Feb 2018 23:22:26 +0100 Subject: [PATCH] gtk: Add GtkMediaStream GtkMediaStream is the new base class for playback of audio and video streams. It implements GdkPaintable for rendering. --- gtk/gtk.h | 1 + gtk/gtkmediastream.c | 1192 ++++++++++++++++++++++++++++++++++++++++++ gtk/gtkmediastream.h | 144 +++++ gtk/meson.build | 2 + 4 files changed, 1339 insertions(+) create mode 100644 gtk/gtkmediastream.c create mode 100644 gtk/gtkmediastream.h diff --git a/gtk/gtk.h b/gtk/gtk.h index 5c43fd6205..dcfcb55dc0 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -137,6 +137,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtkmediastream.c b/gtk/gtkmediastream.c new file mode 100644 index 0000000000..aadec85622 --- /dev/null +++ b/gtk/gtkmediastream.c @@ -0,0 +1,1192 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include "gtkmediastream.h" + +#include "gtkintl.h" + +/** + * SECTION:gtkmediastream + * @Short_description: Display media in GTK + * @Title: GtkMediaStream + * @See_also: #GdkPaintable + * + * #GtkMediaStream is the integration point for media playback inside GTK. + * + * FIXME: Write more about how frameworks should implement this thing and how + * GTK widgets exist (once they do) that consume it. + */ + +typedef struct _GtkMediaStreamPrivate GtkMediaStreamPrivate; + +struct _GtkMediaStreamPrivate +{ + gint64 timestamp; + gint64 duration; + GError *error; + double volume; + + guint has_audio : 1; + guint has_video : 1; + guint playing : 1; + guint ended : 1; + guint seekable : 1; + guint seeking : 1; + guint loop : 1; + guint prepared : 1; + guint muted : 1; +}; + +enum { + PROP_0, + PROP_PREPARED, + PROP_ERROR, + PROP_HAS_AUDIO, + PROP_HAS_VIDEO, + PROP_PLAYING, + PROP_ENDED, + PROP_TIMESTAMP, + PROP_DURATION, + PROP_SEEKABLE, + PROP_SEEKING, + PROP_LOOP, + PROP_MUTED, + PROP_VOLUME, + + N_PROPS, +}; + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +static void +gtk_media_stream_paintable_snapshot (GdkPaintable *paintable, + GdkSnapshot *snapshot, + double width, + double height) +{ +} + +static void +gtk_media_stream_paintable_init (GdkPaintableInterface *iface) +{ + /* We implement the behavior for "no video stream" here */ + iface->snapshot = gtk_media_stream_paintable_snapshot; +} + +G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GtkMediaStream, gtk_media_stream, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, + gtk_media_stream_paintable_init) + G_ADD_PRIVATE (GtkMediaStream)) + +#define GTK_MEDIA_STREAM_WARN_NOT_IMPLEMENTED_METHOD(obj,method) \ + g_critical ("Media stream of type '%s' does not implement GtkMediaStream::" # method, G_OBJECT_TYPE_NAME (obj)) + +static gboolean +gtk_media_stream_default_play (GtkMediaStream *self) +{ + GTK_MEDIA_STREAM_WARN_NOT_IMPLEMENTED_METHOD (self, play); + + return FALSE; +} + +static void +gtk_media_stream_default_pause (GtkMediaStream *self) +{ + GTK_MEDIA_STREAM_WARN_NOT_IMPLEMENTED_METHOD (self, pause); +} + +static void +gtk_media_stream_default_seek (GtkMediaStream *self, + gint64 timestamp) +{ + gtk_media_stream_seek_failed (self); +} + +static void +gtk_media_stream_default_update_audio (GtkMediaStream *self, + gboolean muted, + double volume) +{ +} + +static void +gtk_media_stream_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) + +{ + GtkMediaStream *self = GTK_MEDIA_STREAM (object); + + switch (prop_id) + { + case PROP_PLAYING: + gtk_media_stream_set_playing (self, g_value_get_boolean (value)); + break; + + case PROP_LOOP: + gtk_media_stream_set_loop (self, g_value_get_boolean (value)); + break; + + case PROP_MUTED: + gtk_media_stream_set_muted (self, g_value_get_boolean (value)); + break; + + case PROP_VOLUME: + gtk_media_stream_set_volume (self, g_value_get_double (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_media_stream_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkMediaStream *self = GTK_MEDIA_STREAM (object); + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + switch (prop_id) + { + case PROP_PREPARED: + g_value_set_boolean (value, priv->prepared); + break; + + case PROP_ERROR: + g_value_set_boxed (value, priv->error); + break; + + case PROP_HAS_AUDIO: + g_value_set_boolean (value, priv->has_audio); + break; + + case PROP_HAS_VIDEO: + g_value_set_boolean (value, priv->has_video); + break; + + case PROP_PLAYING: + g_value_set_boolean (value, priv->playing); + break; + + case PROP_ENDED: + g_value_set_boolean (value, priv->ended); + break; + + case PROP_TIMESTAMP: + g_value_set_int64 (value, priv->timestamp); + break; + + case PROP_DURATION: + g_value_set_int64 (value, priv->duration); + break; + + case PROP_SEEKABLE: + g_value_set_boolean (value, priv->seekable); + break; + + case PROP_SEEKING: + g_value_set_boolean (value, priv->seeking); + break; + + case PROP_LOOP: + g_value_set_boolean (value, priv->loop); + break; + + case PROP_MUTED: + g_value_set_boolean (value, priv->muted); + break; + + case PROP_VOLUME: + g_value_set_double (value, priv->volume); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_media_stream_dispose (GObject *object) +{ + GtkMediaStream *self = GTK_MEDIA_STREAM (object); + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_clear_error (&priv->error); + + G_OBJECT_CLASS (gtk_media_stream_parent_class)->dispose (object); +} + +static void +gtk_media_stream_finalize (GObject *object) +{ +#if 0 + GtkMediaStream *self = GTK_MEDIA_STREAM (object); + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); +#endif + + G_OBJECT_CLASS (gtk_media_stream_parent_class)->finalize (object); +} + +static void +gtk_media_stream_class_init (GtkMediaStreamClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (class); + + class->play = gtk_media_stream_default_play; + class->pause = gtk_media_stream_default_pause; + class->seek = gtk_media_stream_default_seek; + class->update_audio = gtk_media_stream_default_update_audio; + + gobject_class->set_property = gtk_media_stream_set_property; + gobject_class->get_property = gtk_media_stream_get_property; + gobject_class->finalize = gtk_media_stream_finalize; + gobject_class->dispose = gtk_media_stream_dispose; + + /** + * GtkMediaStream:prepared: + * + * Whether the stream has finished initializing and existence of + * audio and video is known. + */ + properties[PROP_PREPARED] = + g_param_spec_boolean ("prepared", + P_("Prepared"), + P_("Whether the stream has finished initializing"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:error: + * + * %NULL for a properly working stream or the #GError that the stream is in. + */ + properties[PROP_ERROR] = + g_param_spec_boxed ("error", + P_("Error"), + P_("Error the stream is in"), + G_TYPE_ERROR, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:has-audio: + * + * Whether the stream contains audio + */ + properties[PROP_HAS_AUDIO] = + g_param_spec_boolean ("has-audio", + P_("Has audio"), + P_("Whether the stream contains audio"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:has-video: + * + * Whether the stream contains video + */ + properties[PROP_HAS_VIDEO] = + g_param_spec_boolean ("has-video", + P_("Has video"), + P_("Whether the stream contains video"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:playing: + * + * Whether the stream is currently playing. + */ + properties[PROP_PLAYING] = + g_param_spec_boolean ("playing", + P_("Playing"), + P_("Whether the stream is playing"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:ended: + * + * Set when playback has finished. + */ + properties[PROP_ENDED] = + g_param_spec_boolean ("ended", + P_("Ended"), + P_("Set when playback has finished"), + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:timestamp: + * + * The current presentation timestamp in microseconds. + */ + properties[PROP_TIMESTAMP] = + g_param_spec_int64 ("timestamp", + P_("Timestamp"), + P_("Timestamp in microseconds"), + 0, G_MAXINT64, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:duration: + * + * The stream's duration in microseconds or 0 if unknown. + */ + properties[PROP_DURATION] = + g_param_spec_int64 ("duration", + P_("Duration"), + P_("Timestamp in microseconds"), + 0, G_MAXINT64, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:seekable: + * + * Set unless the stream is known to not support seeking. + */ + properties[PROP_SEEKABLE] = + g_param_spec_boolean ("seekable", + P_("Seekable"), + P_("Set unless seeking is not supported"), + TRUE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:seeking: + * + * Set while a seek is in progress. + */ + properties[PROP_SEEKING] = + g_param_spec_boolean ("seeking", + P_("Seeking"), + P_("Set while a seek is in progress"), + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:loop: + * + * Try to restart the media from the beginning once it ended. + */ + properties[PROP_LOOP] = + g_param_spec_boolean ("loop", + P_("Loop"), + P_("Try to restart the media from the beginning once it ended."), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:muted: + * + * Whether the audio stream should be muted. + */ + properties[PROP_MUTED] = + g_param_spec_boolean ("muted", + P_("Muted"), + P_("Whether the audio stream should be muted."), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtkMediaStream:volume: + * + * Volume of the audio stream. + */ + properties[PROP_VOLUME] = + g_param_spec_boolean ("volume", + P_("Volume"), + P_("Volume of the audio stream."), + 1.0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, properties); +} + +static void +gtk_media_stream_init (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + priv->volume = 1.0; +} + +gboolean +gtk_media_stream_is_prepared (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->prepared; +} + +gboolean +gtk_media_stream_has_audio (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->has_audio; +} + +gboolean +gtk_media_stream_has_video (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->has_video; +} + +void +gtk_media_stream_play (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + + if (priv->error) + return; + + if (priv->playing) + return; + + if (GTK_MEDIA_STREAM_GET_CLASS (self)->play (self)) + { + g_object_freeze_notify (G_OBJECT (self)); + + priv->playing = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PLAYING]); + + if (priv->ended) + { + priv->ended = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENDED]); + } + + g_object_thaw_notify (G_OBJECT (self)); + } +} + +void +gtk_media_stream_pause (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + + /* Don't check for error here because we call this function right + * after setting the error to pause the stream */ + + if (!priv->playing) + return; + + GTK_MEDIA_STREAM_GET_CLASS (self)->pause (self); + + priv->playing = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PLAYING]); +} + +gboolean +gtk_media_stream_get_playing (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->playing; +} + +void +gtk_media_stream_set_playing (GtkMediaStream *self, + gboolean playing) +{ + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + + if (playing) + gtk_media_stream_play (self); + else + gtk_media_stream_pause (self); +} + +gboolean +gtk_media_stream_get_ended (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->ended; +} + +gint64 +gtk_media_stream_get_timestamp (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->timestamp; +} + +/** + * gtk_media_stream_get_duration: + * @self: a #GtkmediaStream + * + * Gets the duration of the stream. If the duration is not known, + * 0 will be returned. + * + * Returns: the duration of the stream or 0 if not known. + **/ +gint64 +gtk_media_stream_get_duration (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->duration; +} + +/** + * gtk_media_stream_is_seekable: + * @self: a #GtkMediaStream + * + * Checks if a stream may be seekable. + * + * This is meant to be a hint. Streams may not allow seeking even if + * this function returns %TRUE. However, if this function returns + * %FALSE, streams are guaranteed to not be seekable and user interfaces + * may hide controls that allow seeking. + * + * It is allowed to call gtk_media_stream_seek() on a non-seekable + * stream, though it will not do anything. + * + * Returns: %TRUE if the stream may support seeking + **/ +gboolean +gtk_media_stream_is_seekable (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->seekable; +} + +/** + * gtk_media_stream_is_seeking: + * @self: a #GtkMediaStream + * + * Checks if there is currently a seek operation going on. + * + * Returns: %TRUE if a seek operation is ongoing. + **/ +gboolean +gtk_media_stream_is_seeking (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->seeking; +} + +/** + * gtk_media_stream_get_error: + * @self: a #GtkMediaStream + * + * If the stream is in an error state, returns the #GError explaining that state. + * Any type of error can be reported here depending on the implementation of the + * media stream. + * + * A media stream in an error cannot be operated on, calls like + * gtk_media_stream_play() or gtk_media_stream_seek() will not have any effect. + * + * #GtkMediaStream itself does not provide a way to unset an error, but + * implementations may provide options. For example, a #GtkMediaFile will unset + * errors when a new source is set with ie gtk_media_file_set_file(). + * + * Returns: (nullable) (transfer none): %NULL if not in an error state or + * the #GError of the stream + **/ +const GError * +gtk_media_stream_get_error (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->error; +} + +/** + * gtk_media_stream_seek: + * @self: a #GtkMediaStream + * @timestamp: timestamp to seek to. + * + * Start a seek operation on @self to @timestamp. If @timestamp is out of range, + * it will be clamped. + * + * Seek operations may not finish instantly. While a seek operation is + * in process, the GtkMediaStream:seeking property will be set. + * + * When calling gtk_media_stream_seek() during an ongoing seek operation, + * the new seek wil override any pending seek. + **/ +void +gtk_media_stream_seek (GtkMediaStream *self, + gint64 timestamp) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + gboolean was_seeking; + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (timestamp >= 0); + + if (priv->error) + return; + + if (!priv->seekable) + return; + + g_object_freeze_notify (G_OBJECT (self)); + + was_seeking = priv->seeking; + priv->seeking = TRUE; + + GTK_MEDIA_STREAM_GET_CLASS (self)->seek (self, timestamp); + + if (was_seeking != priv->seeking) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEEKING]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_media_stream_get_loop: + * @self: a #GtkMediaStream + * + * Returns whether the stream is set to loop. See + * gtk_media_stream_set_loop() for details. + * + * Returns: %TRUE if the stream should loop + **/ +gboolean +gtk_media_stream_get_loop (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->loop; +} + +/** + * gtk_media_stream_set_loop: + * @self: a #GtkMediaStream + * @loop: %TRUE if the stream should loop + * + * Sets whether the stream should loop, ie restart playback from + * the beginning instead of stopping at the end. + * + * Not all streams may support looping, in particular non-seekable + * streams. Those streams will ignore the loop setting and just end. + **/ +void +gtk_media_stream_set_loop (GtkMediaStream *self, + gboolean loop) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + + if (priv->loop == loop) + return; + + priv->loop = loop; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOOP]); +} + +/** + * gtk_media_stream_get_muted: + * @self: a #GtkMediaStream + * + * Returns whether the audio for the stream is muted. + * See gtk_media_stream_set_muted() for details. + * + * Returns: %TRUE if the stream is muted + **/ +gboolean +gtk_media_stream_get_muted (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->muted; +} + +/** + * gtk_media_stream_set_muted: + * @self: a #GtkMediaStream + * @muted: %TRUE if the stream should be muted + * + * Sets whether the audio stream should be muted. Muting a stream will + * cause no audio to be played, but it does not modify the volume. + * This means that muting and then unmuting the stream will restore + * the volume settings. + * + * If the stream has no audio, calling this function will still work + * but it will not have an audible effect. + **/ +void +gtk_media_stream_set_muted (GtkMediaStream *self, + gboolean muted) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + + if (priv->muted == muted) + return; + + priv->muted = muted; + + GTK_MEDIA_STREAM_GET_CLASS (self)->update_audio (self, priv->muted, priv->volume); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MUTED]); +} + +/** + * gtk_media_stream_get_volume: + * @self: a #GtkMediaStream + * + * Returns the volume of the audio for the stream. + * See gtk_media_stream_set_volume() for details. + * + * Returns: volume of the stream from 0.0 to 1.0 + **/ +double +gtk_media_stream_get_volume (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_val_if_fail (GTK_IS_MEDIA_STREAM (self), FALSE); + + return priv->volume; +} + +/** + * gtk_media_stream_set_volume: + * @self: a #GtkMediaStream + * @volume: New volume of the stream from 0.0 to 1.0 + * + * Sets the volume of the audio stream. This function call will work even if + * the stream is muted. + * + * The given @volume should range from 0.0 for silence to 1.0 for as loud as + * possible. Values outside of this range will be clamped to the nearest + * value. + * + * If the stream has no audio or is muted, calling this function will still + * work but it will not have an immediate audible effect. When the stream is + * unmuted, the new volume setting will take effect. + **/ +void +gtk_media_stream_set_volume (GtkMediaStream *self, + double volume) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + + volume = CLAMP (volume, 0.0, 1.0); + + if (priv->volume == volume) + return; + + priv->volume = volume; + + GTK_MEDIA_STREAM_GET_CLASS (self)->update_audio (self, priv->muted, priv->volume); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VOLUME]); +} + +/** + * gtk_media_stream_prepared: + * @self: a #GtkMediaStream + * @has_audio: %TRUE if the stream should advertise audio support + * @has_video: %TRUE if the stream should advertise video support + * @seekable: %TRUE if the stream should advertise seekability + * @duration: The duration of the stream or 0 if unknown + * + * Called by #GtkMediaStream implementations to advertise the stream + * being ready to play and providing details about the stream. + * + * Note that the arguments are hints. If the stream implementation + * cannot determine the correct values, it is better to err on the + * side of caution and return %TRUE. User interfaces will use those + * values to determine what controls to show. + * + * This function may not be called again until the stream has been + * reset via gtk_media_stream_unprepared(). + **/ +void +gtk_media_stream_prepared (GtkMediaStream *self, + gboolean has_audio, + gboolean has_video, + gboolean seekable, + gint64 duration) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (!gtk_media_stream_is_prepared (self)); + + g_object_freeze_notify (G_OBJECT (self)); + + if (priv->has_audio != has_audio) + { + priv->has_audio = has_audio; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_AUDIO]); + } + if (priv->has_video != has_video) + { + priv->has_video = has_video; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_VIDEO]); + } + if (priv->seekable != seekable) + { + priv->seekable = seekable; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEEKABLE]); + } + if (priv->duration != duration) + { + priv->duration = duration; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DURATION]); + } + + priv->prepared = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PREPARED]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_media_stream_unprepared: + * @self: a #GtkMediaStream + * + * Resets a given media stream implementation. gtk_media_stream_prepared() + * can now be called again. + * + * This function will also reset any error state the stream was in. + **/ +void +gtk_media_stream_unprepared (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (gtk_media_stream_is_prepared (self)); + + g_object_freeze_notify (G_OBJECT (self)); + + gtk_media_stream_pause (self); + + if (priv->has_audio) + { + priv->has_audio = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_AUDIO]); + } + if (priv->has_video) + { + priv->has_video = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_VIDEO]); + } + if (priv->seekable) + { + priv->seekable = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEEKABLE]); + } + if (priv->seeking) + { + priv->seeking = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEEKING]); + } + if (priv->duration != 0) + { + priv->duration = 0; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DURATION]); + } + if (priv->timestamp != 0) + { + priv->timestamp = 0; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TIMESTAMP]); + } + if (priv->error) + { + g_clear_error (&priv->error); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + } + + priv->prepared = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PREPARED]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_media_stream_gerror: + * @self: a #GtkMediaStream + * @error: (transfer full): the #GError to set + * + * Sets @self into an error state. This will pause the stream + * (you can check for an error via gtk_media_stream_get_error() in + * your GtkMediaStream.pause() implementation), abort pending seeks + * and mark the stream as prepared. + * + * To unset an error, the stream must be reset via a call to + * gtk_media_stream_unprepared(). + **/ +void +gtk_media_stream_gerror (GtkMediaStream *self, + GError *error) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (gtk_media_stream_get_error (self) == NULL); + g_return_if_fail (error != NULL); + + g_object_freeze_notify (G_OBJECT (self)); + + priv->error = error; + + gtk_media_stream_pause (self); + + if (!priv->prepared) + { + priv->prepared = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PREPARED]); + } + + if (priv->seeking) + gtk_media_stream_seek_failed (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ERROR]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_media_stream_error: + * @self: a #GtkMediaStream + * @domain: error domain + * @code: error code + * @format: printf()-style format for error message + * @...: parameters for message format + * + * Sets @self into an error state using a printf()-style format string. + * + * This is a utility function that calls gtk_media_stream_gerror(). See + * that function for details. + **/ +void +gtk_media_stream_error (GtkMediaStream *self, + GQuark domain, + gint code, + const gchar *format, + ...) +{ + GError *error; + va_list args; + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (domain != 0); + g_return_if_fail (format != NULL); + + va_start (args, format); + error = g_error_new_valist (domain, code, format, args); + va_end (args); + + gtk_media_stream_gerror (self, error); +} + +/** + * gtk_media_stream_error: + * @self: a #GtkMediaStream + * @domain: error domain + * @code: error code + * @format: printf()-style format for error message + * @args: #va_list of parameters for the message format + * + * Sets @self into an error state using a printf()-style format string. + * + * This is a utility function that calls gtk_media_stream_gerror(). See + * that function for details. + */ +void +gtk_media_stream_error_valist (GtkMediaStream *self, + GQuark domain, + gint code, + const gchar *format, + va_list args) +{ + GError *error; + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (domain != 0); + g_return_if_fail (format != NULL); + + error = g_error_new_valist (domain, code, format, args); + + gtk_media_stream_gerror (self, error); +} + +/** + * gtk_media_stream_update: + * @self: a #GtkMediaStream + * @timestamp: the new timestamp + * + * Media stream implementations should regularly call this function to + * update the timestamp reported by the stream. It is up to + * implementations to call this at the frequency they deem appropriate. + **/ +void +gtk_media_stream_update (GtkMediaStream *self, + gint64 timestamp) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + + g_object_freeze_notify (G_OBJECT (self)); + + /* Timestamp before duration is important here. + * This way the duration notify will be emitted first which will + * make GtkMediaControls update adjustment->upper so that the + * timestamp notify will cause the timestamp to not be clamped. + */ + if (priv->timestamp != timestamp) + { + priv->timestamp = timestamp; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TIMESTAMP]); + } + if (priv->duration > 0 && timestamp > priv->duration) + { + priv->duration = priv->timestamp; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DURATION]); + } + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_media_stream_ended: + * @self: a #GtkMediaStream + * + * Pauses the media stream and marks it as ended. This is a hint only, calls + * to GtkMediaStream.play() may still happen. + **/ +void +gtk_media_stream_ended (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (!gtk_media_stream_get_ended (self)); + + g_object_freeze_notify (G_OBJECT (self)); + + gtk_media_stream_pause (self); + + priv->ended = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENDED]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_media_stream_seek_success: + * @self: a #GtkMediaStream + * + * Ends a seek operation started via GtkMediaStream.seek() successfully. + * This function will unset the GtkMediaStream:ended property if it was + * set. + * + * See gtk_media_stream_seek_failed() for the other way of + * ending a seek. + **/ +void +gtk_media_stream_seek_success (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (gtk_media_stream_is_seeking (self)); + + g_object_freeze_notify (G_OBJECT (self)); + + priv->seeking = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEEKING]); + + if (priv->ended) + { + priv->ended = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENDED]); + } + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtk_media_stream_seek_failed: + * @self: a #GtkMediaStream + * + * Ends a seek operation started via GtkMediaStream.seek() as a failure. + * This will not cause an error on the stream and will assume that + * playback continues as if no seek had happened. + * + * See gtk_media_stream_seek_success() for the other way of + * ending a seek. + **/ +void +gtk_media_stream_seek_failed (GtkMediaStream *self) +{ + GtkMediaStreamPrivate *priv = gtk_media_stream_get_instance_private (self); + + g_return_if_fail (GTK_IS_MEDIA_STREAM (self)); + g_return_if_fail (gtk_media_stream_is_seeking (self)); + + priv->seeking = FALSE; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEEKING]); +} + diff --git a/gtk/gtkmediastream.h b/gtk/gtkmediastream.h new file mode 100644 index 0000000000..3029696518 --- /dev/null +++ b/gtk/gtkmediastream.h @@ -0,0 +1,144 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#ifndef __GTK_MEDIA_STREAM_H__ +#define __GTK_MEDIA_STREAM_H__ + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_MEDIA_STREAM (gtk_media_stream_get_type ()) + +GDK_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (GtkMediaStream, gtk_media_stream, GTK, MEDIA_STREAM, GObject) + +struct _GtkMediaStreamClass +{ + GObjectClass parent_class; + + gboolean (* play) (GtkMediaStream *self); + void (* pause) (GtkMediaStream *self); + void (* seek) (GtkMediaStream *self, + gint64 timestamp); + void (* update_audio) (GtkMediaStream *self, + gboolean muted, + double volume); + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); + void (*_gtk_reserved3) (void); + void (*_gtk_reserved4) (void); + void (*_gtk_reserved5) (void); + void (*_gtk_reserved6) (void); + void (*_gtk_reserved7) (void); + void (*_gtk_reserved8) (void); +}; + +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_is_prepared (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +const GError * gtk_media_stream_get_error (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_has_audio (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_has_video (GtkMediaStream *self); + +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_play (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_pause (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_get_playing (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_set_playing (GtkMediaStream *self, + gboolean playing); +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_get_ended (GtkMediaStream *self); + +GDK_AVAILABLE_IN_ALL +gint64 gtk_media_stream_get_timestamp (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +gint64 gtk_media_stream_get_duration (GtkMediaStream *self); + +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_is_seekable (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_is_seeking (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_seek (GtkMediaStream *self, + gint64 timestamp); +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_get_loop (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_set_loop (GtkMediaStream *self, + gboolean loop); +GDK_AVAILABLE_IN_ALL +gboolean gtk_media_stream_get_muted (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_set_muted (GtkMediaStream *self, + gboolean muted); +GDK_AVAILABLE_IN_ALL +double gtk_media_stream_get_volume (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_set_volume (GtkMediaStream *self, + double volume); + +/* for implementations only */ +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_prepared (GtkMediaStream *self, + gboolean has_audio, + gboolean has_video, + gboolean seekable, + gint64 duration); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_unprepared (GtkMediaStream *self); + +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_update (GtkMediaStream *self, + gint64 timestamp); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_ended (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_seek_success (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_seek_failed (GtkMediaStream *self); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_gerror (GtkMediaStream *self, + GError *error); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_error (GtkMediaStream *self, + GQuark domain, + gint code, + const gchar *format, + ...) G_GNUC_PRINTF (4, 5); +GDK_AVAILABLE_IN_ALL +void gtk_media_stream_error_valist (GtkMediaStream *self, + GQuark domain, + gint code, + const gchar *format, + va_list args) G_GNUC_PRINTF (4, 0); + +G_END_DECLS + +#endif /* __GTK_MEDIA_STREAM_H__ */ diff --git a/gtk/meson.build b/gtk/meson.build index 619b709b1b..630c2e0bed 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -251,6 +251,7 @@ gtk_public_sources = files([ 'gtkliststore.c', 'gtklockbutton.c', 'gtkmain.c', + 'gtkmediastream.c', 'gtkmenu.c', 'gtkmenubar.c', 'gtkmenubutton.c', @@ -480,6 +481,7 @@ gtk_public_headers = files([ 'gtkliststore.h', 'gtklockbutton.h', 'gtkmain.h', + 'gtkmediastream.h', 'gtkmenu.h', 'gtkmenubar.h', 'gtkmenubutton.h', -- 2.30.2